# vim:ts=4:sw=4:noet:
""" Object memory management and serialisation support """

import os
import pickle
import spy
import datetime
import logging

IMAGE_VERSION = 1
CORE_DEPENDANCIES = [
	"Array",
	"Block",
	"ByteArray",
	"Char",
	"Class",
	"Method",
	"OrderedArray",
	"Object",
	"True",
	"False",
	"Undefined",
	"Symbol",
	"true",
	"false",
	"nil",
	"mainClass"
]

import StringIO
import sys

class FoxUnpickler(pickle.Unpickler):
	PICKLE_SAFE = {
		'copy_reg': set(['_reconstructor']),
		'__builtin__': set(['object']),
		'datetime' : set(['datetime']),
		'c.compiler.mem' : set(['Mem', 'Image']),
		'c.compiler.spy' : "all",
	}
	def find_class(self, module, name):
		if not module in self.PICKLE_SAFE:
			raise pickle.UnpicklingError('Attempting to unpickle unsafe module %s' % module)
		__import__(module)
		mod = sys.modules[module]
		if self.PICKLE_SAFE[module]!="all":
			if not name in self.PICKLE_SAFE[module]:
				raise pickle.UnpicklingError('Attempting to unpickle unsafe class %s' % name)
		return getattr(mod, name)

	@classmethod
	def loads(cls, st):
		return cls(StringIO.StringIO(st)).load()

class Image:
	def __init__(self, globals, symbols, date=None):
		self.globals = globals
		self.symbols = symbols
		self.version = IMAGE_VERSION
		if date:
			self.date = date
		else:
			self.date = datetime.datetime.now()

class Memory:
	_instance = None
	def __init__(self, status):
		self.status = status
		self.globals = {}
		self.symbols = {}
		self.haveGlobals = False
		self.date = None
		self.defaultDate = None
		Memory._instance = self
		self.messageTable = None
		self.oldImage = False

	def loadDefaultDate(self, filename):
		if os.path.exists(filename):
			f = file(filename, "rb")
			self.defaultDate = pickle.load(f)
			f.close()
		else:
			self.defaultDate = datetime.datetime(1970,1,1)

	def reset(self):
		self.globals = {}
		self.symbols = {}
		self.haveGlobals = False

	def addGlobal(self, name, value):
		self.globals[name] = value
		if self.haveGlobals:
			self.globalValues.add(name, value)
		return value
	
	def getGlobal(self, name):
		if name in self.globals:
			return self.globals[name]
		return None

	def removeGlobal(self, name):
		if name in self.globals:
			del self.globals[name]
		if self.globalValues.has(name):
			self.globalValues.remove(name)

	def newSymbol(self, name):
		if name in self.symbols:
			return self.symbols[name]
		sym = spy.SpySymbol(self, name)
		self.symbols[name] = sym
		return sym

	def createBase(self):
		self.nilObject = spy.SpyObject(self, None)
		self.SymbolClass = spy.SpyClass(self, "Symbol")
		self.addGlobal("Symbol", self.SymbolClass)

		NilClass = self.addGlobal("Undefined", spy.SpyClass(self, "Undefined"))
		self.nilObject.cls = NilClass
		self.addGlobal("nil", self.nilObject)
		
		ObjectClass = self.addGlobal("Object", spy.SpyClass(self, "Object"))
		MetaObjectClass = self.addGlobal("MetaObject", spy.SpyClass(self, "MetaObject"))
		ObjectClass.parent = self.nilObject

		ClassClass = self.addGlobal("Class", spy.SpyClass(self, "Class", ObjectClass))
		MetaClassClass = self.addGlobal("MetaClass", spy.SpyClass(self, "MetaClass"))
		MetaClassClass.cls = ClassClass

		self.addGlobal("Block", spy.SpyClass(self, "Block"))
		self.addGlobal("Method", spy.SpyClass(self, "Method"))
		self.addGlobal("Char", spy.SpyClass(self, "Char"))
		self.SmallIntClass = self.addGlobal("SmallInt", spy.SpyClass(self, "SmallInt"))
		self.SmallIntClass.size = 6
		self.addGlobal("Integer", spy.SpyClass(self, "Integer"))
		TrueClass = self.addGlobal("True", spy.SpyClass(self, "True"))
		self.trueObject = self.addGlobal("true", spy.SpyObject(self, TrueClass))
		FalseClass = self.addGlobal("False", spy.SpyClass(self, "False"))
		self.falseObject = self.addGlobal("false", spy.SpyObject(self, FalseClass))
		self.addGlobal("Array", spy.SpyClass(self, "Array"))
		self.addGlobal("ByteArray", spy.SpyClass(self, "ByteArray"))
		self.addGlobal("OrderedArray", spy.SpyClass(self, "OrderedArray"))
		self.addGlobal("String", spy.SpyClass(self, "String"))
		self.addGlobal("Dictionary", spy.SpyClass(self, "Dictionary"))

		self.globalValues = spy.SpyDictionary(self)
		self.addGlobal("mainClass", NilClass)
		self.haveGlobals = True
		self.addGlobal("MetaObject", MetaObjectClass)

	def fileOut(self, basedir):
		for k in self.globalValues.pdict:
			v = self.globalValues.pdict[k]
			v.fileOut(basedir)

	def save(self, filename, infofile=None):
		f = file(filename, "wb")
		img = Image(self.globalValues, self.symbols, self.date)
		pickle.dump(img, f, 2)
		size = f.tell()
		f.close()
		if infofile:
			f = file(infofile, "wb")
			pickle.dump(img.date, f, 2)
			f.close()
		return size
	
	def load(self, filename):
		self.reset()
		f = file(filename, "rb")
		try:
			img = FoxUnpickler.loads(f.read())
		except Exception, e:
			print "Fatal deserialisation error", e
		if img.version < IMAGE_VERSION:
			logging.warn("Older image version, expect breakage")
		if img.version > IMAGE_VERSION:
			logging.warn("Newer image version, expect breakage")
		if hasattr(img, "date"):
			logging.info("Image creation time: %s" % img.date)
		self.globalValues = img.globals
		self.symbols = img.symbols
		self.date = img.date
		self.oldImage = (self.defaultDate > self.date)
		if self.oldImage:
			logging.warn("Image is older than default")
		self.haveGlobals = True
		for gn in self.globalValues.pdict:
			self.addGlobal(gn, self.globalValues.pdict[gn])
		self.nilObject = self.globalValues.get("nil")
		self.trueObject = self.globalValues.get("true")
		self.falseObject = self.globalValues.get("false")
		f.close()
		self.generateMessageTable()

	def generateMessageTable(self):
		self.messageTable = {}
		visited = {}
		for gn in self.globals:
			cls = self.globals[gn]
			if isinstance(cls, spy.SpyClass):
				if cls.name not in visited:
					visited[cls.name] = 1
					for meth in cls.methods.pdict:
						if meth in self.messageTable:
							self.messageTable[meth] += 1
						else:
							self.messageTable[meth] = 1

	def addMessage(self, msg):
		if msg in self.messageTable:
			self.messageTable[msg] += 1
		else:
			self.messageTable[msg] = 1

	def removeMessage(self, msg):
		if self.messageTable[msg] > 1:
			self.messageTable[msg] -= 1
		else:
			del self.messageTable[msg]

	def getMainClass(self):
		mainClass = None
		for g in self.globals:
			glb = self.globals[g]
			if isinstance(glb, spy.SpyClass):
				if glb.methods.has("main"):
					mainClass = glb
					break
		return mainClass

	def readyForSave(self):
		for g in self.globals:
			self.globalValues.add(g, self.globals[g])

		mainClass = self.nilObject
		for g in self.globals:
			glb = self.globals[g]
			if isinstance(glb, spy.SpyClass):
				if glb.methods.has("main"):
					mainClass = glb
					break

		self.globalValues.add("mainClass", mainClass)
		bootClass = self.globalValues.get("nil").cls
		bootClass.methods.get("boot").literals[0] = mainClass
		return mainClass != self.nilObject

	def getGlobalValues(self):
		gusage = {}
		for g in self.globals:
			gusage[g] = Dependancy(g)
		# create core dependancy list
		core = Dependancy("!CORE", map(lambda x: gusage[x],CORE_DEPENDANCIES))
		# iterate through globals populating dependancy tree
		for g in self.globals:
			glob = self.globals[g]
			if g[:4] != "Meta" and isinstance(glob, spy.SpyClass):
				thisdep = gusage[g]
				# add meta class dependancy
				if "Meta"+g in gusage:
					thisdep.dependsOn(gusage["Meta"+g])
				# add superclass dependancy
				if isinstance(glob.parent, spy.SpyClass) and glob.parent.name in gusage:
					thisdep.dependsOn(gusage[glob.parent.name])
				# add method literal dependancies
				for meth in glob.methods.pdict.values():
					clslits = [l for l in meth.literals if isinstance(l, spy.SpyClass)]
					for c in clslits:
						thisdep.dependsOn(gusage[c.name])
		# traverse unused class dependancies
		unused = []
		for u in [g for g in gusage if gusage[g].countDependedOnBy()==0]:
			expandUnused(gusage, u, None, unused, [])
		# create globals array skipping meta- and unused classes
		glbs = spy.SpyDictionary(self)
		for g in self.globals:
			if g[:4] != "Meta" and g not in unused:
				glbs.add(g, self.globals[g])
		return glbs

def expandUnused(gusage, name, parent, unused, visited):
	if name in visited:
		return
	visited.append(name)
	u = gusage[name]
	if (u.countDependedOnBy()==1 and u._isDependedOnBy[0].value==parent) or (not parent):
		unused.append(name)
	else:
		return
	for dep in u._dependsOn:
		expandUnused(gusage, dep.value, name, unused, visited)
	
class Dependancy:
	def __init__(self, value, deps=[]):
		self.value = value
		self._dependsOn = []
		self._isDependedOnBy = []
		for dep in deps:
			self.dependsOn(dep)

	def dependsOn(self, dep):
		if dep not in self._dependsOn:
			self._dependsOn.append(dep)
			dep.isDependedOnBy(self)

	def isDependedOnBy(self, dep):
		if dep not in self._isDependedOnBy:
			self._isDependedOnBy.append(dep)

	def countDependsOn(self):
		return len(self._dependsOn)

	def countDependedOnBy(self):
		return len(self._isDependedOnBy)

	def __repr__(self):
		return "<dep '%s' [%d/%d]>" % (self.value, len(self._dependsOn), len(self._isDependedOnBy))
